"""
cdn.core

Implements core functionality of the CDN server not tied to its APIs.
"""
### Start of imports

# This line just better work...
from jackson import debug
import jackson.extjson.ext_json as json

# There's debug stuff for the rest
debug.pre_import(__name__)

debug.importing(__name__, 'settings from django.conf', 2)
from django.conf import settings

debug.importing(__name__, 'ContentEntry, StateEntry from jackson.cdn.models')
from jackson.cdn.models import ContentEntry, StateEntry

debug.importing(__name__, 'key_in_range from jackson.utils')
from jackson.utils import key_in_range

debug.importing(__name__, 'net from jackson.cdn')
from jackson.cdn import net

debug.importing(__name__, 'Server from jackson.chord')
from jackson.chord import Server

debug.importing(__name__, 'urllib2, os')
import urllib2
import os

debug.post_import(__name__)

### End of imports

def clear_db():
    """ Empties the database.
    @author: ep
    @rtype: None
    """
    context = [__name__, 'clear_db()']
    debug.enter(context, None, 3)
    debug.comment(context, 'Clearing ContentEntry table', 3)
    ContentEntry.objects.all().delete()
    debug.comment(context, 'Clearing StateEntry table', 3)
    StateEntry.objects.all().delete()
    debug.leave(context, None, 3)
    return


def delete_content(entry):
    """ Deletes the file specified by entry from disk.
    @author: ep
    @rtype: True on success, False otherwise
    @note: only requires entry.key
    """
    context = [__name__, 'delete_content()']
    if entry == None:
        debug.enter(context, None)
        debug.exception(context, 'called with entry = None')
        debug.leave(context, None)
        return False

    ans = delete_content_by_key(entry.key)
    debug.leave(context, { 'success?' : ans })
    return ans


def delete_content_by_key(the_key):
    context = [__name__, 'delete_content_by_key()']
    debug.enter(context, { 'key' : the_key })
    try:
        debug.comment(context,"removing the file: + " + str(the_key))
        os.remove(settings.LIGHTTPD_DOC_ROOT + the_key)
        debug.leave(context, { 'success?': True })
        return True
    except Exception, e:
        debug.exception(context,"problem in delete_content_by_key : "+str(e))
        debug.leave(context, { 'OSError': e })
        return False


def get_content(entry):
    """ Attempts to download the content associated specified by entry
    @author: ep
    @param entry: ContentEntry with status 'I'
    @return: True if we successfully acquired content we need, False otherwise
    """
    context = [__name__, 'get_content()']
    debug.enter(context, { 'entry' : entry ,'key_to_download' : entry.key })

    # Validate argument
    if entry == None:
        estring = "argument problem: entry is None"
        debug.exception(context, estring)
        debug.leave(context, { 'exception' : estring })
        raise Exception("cdn.sync.get_content(): argument problem")
    
    if entry:
        # Get the URL to download the content from
        if isinstance(entry.server, str):
            debug.comment(context,"loading this string into json: " + entry.server)
            s = json.loads(entry.server)    
        else:
            s = entry.server
        url = net.get_url(s, entry.key)
        
        if url != "DNE":
            entry.status = 'T'
            entry.save()

            # Attempt to download the content from the network, and write to disk
            fq_filename = settings.LIGHTTPD_DOC_ROOT + str(entry.key)
            result = net.download_content(url, fq_filename)
                
            # Update our entry from database
            #@note: is this necessary?
            #@note: idk, but let's leave it until we know it isn't needed -ep
            entry = ContentEntry.objects.get(key=entry.key)
            
            # Check if the download was successful; 
            # @todo: check the md5 checksum
            if not result:
                if entry.status == 'D':
                    entry.delete()
                elif entry.status == 'T':
                    entry.status = 'I'
                    entry.save()
                    penalize(entry)
                else:
                    estring = 'bad state change from T to ' + entry.status
                    debug.exception(context, estring)
                    debug.leave(context, { 'action' : estring })
                    raise Exception, "In jackson.cdn.core.get_content: Unexpected state change from T to " + entry.status
                debug.leave(context, { 'action': 'download failed!' })
                return False

            # Check if the content was forwarded while we were downloading it
            if entry.status == 'D':
                debug.comment(context, "entry was forwarded")
                delete_content(entry)
                entry.delete()
                debug.leave(context, { 'action' : "deleting the entry"})
                return False
            elif entry.status != 'T':
                debug.leave(context, { 'action' : "some weird error in core.get_content()"})
                raise Exception, "In jackson.cdn.core.get_content: Unexpected state change from T to " + entry.status

            # Handle successful download!
            entry.url = 'http://' + settings.MY_IP + ':' + str(settings.MY_PORT) + '/' + entry.key
            entry.status = 'H'
            entry.save()
            net.confirm_distribution(s, entry.key)
            debug.leave(context, { 'action' : "the stuff is here, marking it as such"})
            return True
        else:
            estring = "URL didn't exist. Is entry.server stale?"
            debug.exception(context, estring)
            debug.leave(context, { 'exception': estring })
            raise Exception, 'jackson.cdn.core.get_content: ' + estring
    else:
        estring = "argument problem: entry is None"
        debug.exception(context, estring)
        debug.leave(context, { 'exception': estring })
        raise Exception("cdn.core.get_content(): " + estring)

def get_url(entry):
    """ just pull out the url for some key. return False on except
    """
    context = [__name__, 'get_url()']
    debug.enter(context, None)
    try:
        entry = ContentEntry(key=the_key)
        r = entry.url
    except:
        r= False
    debug.leave(context, None)
    return r

def get_content_old(entry):
    """ Attempts to download the content associated specified by entry
    @author: ep
    @param entry: ContentEntry of type 4
    @return: True if we successfully acquired content we need, False otherwise
    """
    context = [__name__, 'get_content()']
    debug.enter(context, { 'entry' : entry ,'key_to_download' : entry.key })
    if entry == None:
        estring = "argument problem: entry is None"
        debug.exception(context, estring)
        debug.leave(context, { 'exception' : estring })
        raise Exception("cdn.sync.get_content(): argument problem")
    
    # Get the URL to download the content from
    # entry.server is a string
    debug.comment(context,"entries now have server fields, instead of a string.")
    debug.comment(context,"so, this should be the Server class: " + str(entry.server.__class__))
    s = entry.server #json.loads(entry.server)    

    url = net.get_url(s, entry.key)
    
    # Check that we got a URL
    #@see: jackson.cdn.client_api.get_url
    if url == "DNE":
        # If we did not, then something's wrong
        estring = "URL didn't exist. Is entry.server stale?"
        debug.exception(contest, estring)
        debug.leave(context, { 'action' : estring})
        raise Exception, 'In jackson.cdn.core.get_content: Could not retrieve a valid URL. Is entry.server stale?'
    
    # Change status to Transferring
    entry.status = 'T'
    entry.save()
    
    # Attempt to download the content from the network, and write to disk
    # if this fails, it returns false
    #@note: merging net.download_content into cdn.core.get_content
    fq_filename = settings.LIGHTTPD_DOC_ROOT + str(entry.key)
    #result = net.download_content(url, fq_filename)

    
    # Update our entry from database
    #@note: is this necessary?
    entry = ContentEntry.objects.get(key=entry.key)
    
    # Check if the download was successful; 
    # @todo: check the md5 checksum
    if not result:
        # If the download wasn't successful...
        if entry.status == 'D':
            # If this content was marked Delete...
            entry.delete()
        elif entry.status == 'T':
            # Otherwise, restore it to Incoming
            entry.status = 'I'
            entry.save()
            # and penalize for failed download
            penalize(entry)
        else:
            debug.leave(context, { 'action' : "bad state change from T to " + entry.status})
            raise Exception, "In jackson.cdn.core.get_content: Unexpected state change from T to " + entry.status
        return False
    # Check if the content was forwarded while we were downloading it
    if entry.status == 'D':
        # If it was, delete the file and the entry
        delete_content(entry)
        entry.delete()
        debug.leave(context, { 'action' : "deleting the entry"})
        return False
    elif entry.status == 'T':
        # If it wasn't, then we're golden: change to Here
        entry.status = 'H'
        entry.save()
        debug.leave(context, { 'action' : "the stuff is here, marking it as such"})
        return True
    else:
        debug.leave(context, { 'action' : "some weird error in core.get_content()"})
        raise Exception, "In jackson.cdn.core.get_content: Unexpected state change from T to " + entry.status

def get_content_in_key_range(low, high):
    """ Returns a qs consisting of all Content entries with key in range (low, high]
    @author: ep
    @param low: key at start of range
    @param high: key at end of range
    @rtype: QuerySet
    @note: if low == high, this function interprets the interval to be the whole keyspace
    """
    context = [__name__, 'get_content_in_key_range()', []]
    debug.enter(context, { 'low' : low, 'high' : high }, 4)
    if low < high:
        debug.comment(context, 'A normal, linear interval', 4)
        # The normal, linear interval
        qs = ContentEntry.objects.filter(key__lte=high).filter(key__gt=low)
    elif high < low:
        debug.comment(context, 'Complement of a linear interval', 4)
        # The complement of a linear interval
        qs = ContentEntry.objects.all().exclude(key__range=(high,low))
    else:
        debug.comment(context, 'All of key space', 4)
        qs = ContentEntry.objects.all()
    debug.leave(context, { 'qs' : qs }, 4)
    return qs

def get_state(the_state):
    """ Returns the value associated to given state if it exists, None otherwise.
    @author: ep
    @rtype: a string or None
    """
    try:
        entry = StateEntry.objects.get(state=the_state)
        return entry.value
    except:
        return None

def i_am_active():
    """ Returns True if this server is active, False otherwise
    @author: ep
    @rtype: True or False
    """
    return get_state('status') == 'Active'

def i_am_responsible_for(the_key):
    """ Returns True if the_key is within our sector of the ring.
    @author: ep
    @rtype: True or False
    """
    return key_in_range(the_key, get_state('low'), get_state('key'))

def md5_checksum(file):
    """ Computes the md5 hex digest of file.
    @author: ep
    @param file: a django.core.files.File object
    @rtype: string
    @return: an md5 hexdigest if successful, or the empty string '' if not
    """
    context = [__name__, 'md5_checksum()']
    debug.enter(context, None)
    debug.comment(context, 'NOTE: File handling currently disabled')
    debug.comment(context, 'NOTE: URL enabled, does not affect this function')
    ### Remove below lines when enabling file handling
    debug.leave(context, { 'checksum' : '' })
    return ''
    ### Remove above lines when enabling file handling
    m = hashlib.md5()
    try:
        file.open(mode='r')
        for chunk in file.chunks():
            m.update(chunk)
        file.close()
        return m.hexdigest()
    except:
        raise Exception("cdn.core.md5_checksum: hash problem or issue reading a file")

def penalize(entry):
    """ Penalize this entry's urgency due to a failed download.
    @author: ep
    @param entry: a ContentEntry
    @rtype: None
    """
    entry.urgency = entry.urgency - settings.DOWNLOAD_FAILURE_PENALTY
    if entry.urgency < 0:
        entry.urgency = 0
    entry.save()
    return

def update_state(state, value):
    """ Sets state to value in the StateEntry table.
    @author: ep
    @rtype: None
    """
    state_entry = StateEntry(state, value)
    state_entry.save()
    return
